#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <utmpx.h>
#include <stdbool.h>
#include <math.h>
#ifdef AFFINITY
#include <sched.h>
#endif
#include "channel.h"


#define EPS 0.00000000000002
#define MAX_IT 25
#define MAX 10000.15234
int core_mapping[16] = { 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7 };
int affty[]= {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
int nWorkers;
int matrixSize;
int matrixH, matrixW; //height and width of the matrix

void worker(int id, key_t n_in, key_t n_out, key_t s_in, key_t s_out,key_t **red_k)
{
    int i,j;

#ifdef AFFINITY
    cpu_set_t mask;
    /* CPU_ZERO initializes all the bits in the mask to zero. */
    CPU_ZERO(&mask);
    /* CPU_SET sets only the bit corresponding to cpu. */
    CPU_SET(affty[id], &mask);
    /* sched_setaffinity returns 0 in success */
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1)
    {
        printf("WARNING: Could not set CPU Affinity, continuing...\n");
    }
    printf("Hello, i'm the worker %d: nIn=%d, nOut=%d, sIn=%d, sOut=%d. I run on %d\n",id,n_in,n_out,s_in,s_out,sched_getcpu());
#endif
    //channels attach
    Channel nord_in=NULL, nord_out=NULL, sud_in=NULL,sud_out=NULL;
    if(n_in!=0)//not the first worker
    {
        nord_in=LCattach(n_in);
        nord_out=LCattach(n_out);
    }
    if(s_in!=0) //not the last
    {
        sud_in=LCattach(s_in);
        sud_out=LCattach(s_out);
    }

    //channel for reduce
    Channel *red_in=(Channel *)malloc(sizeof(Channel)*nWorkers);
    Channel *red_out=(Channel *)malloc(sizeof(Channel)*nWorkers);
    for(i=0; i<nWorkers; i++)
    {
        if(i!=id) //for all the workers except me
        {
            //printf("[worker %d] attach reduce input=%d, output=%d\n",id,red_k[i][id],red_k[id][i]);
            red_in[i]=LCattach(red_k[i][id]);
            red_out[i]=LCattach(red_k[id][i]);
        }
    }
    //generation of the random matrix

    srand(time(NULL));
    //allocate the space for the matrix (two, one is for the matrix at the actual
    // step, the other is for the one that is being computed)
    double **m=(double **)malloc((matrixH)*sizeof(double *));
    for(i=0; i<matrixH; i++)
        m[i]=(double *)(malloc((matrixW)*sizeof(double)));
    double **m_next=(double **)malloc((matrixH)*sizeof(double *));
    for(i=0; i<matrixH; i++)
        m_next[i]=(double *)(malloc((matrixW)*sizeof(double)));
    //generate the matrix elements
    for(i=0; i<matrixH; i++)
        for(j=0; j<matrixW; j++)
            m[i][j]=fmod(rand(),MAX)+1;

    int it=0;
    bool conv,stop;
    double north,south,est,west;
    do
    {
        //send the frontiers to the neighbor
        if(id>0)
            LCsend(nord_out,(char *)m[0]); //send nord frontier
        if(id<nWorkers-1)
            LCsend(sud_out,(char *)m[matrixH-1]); //send south frontier
        //receive the frontiers from the neighbor
        double *n=NULL,*s=NULL;
        if(id>0)
            n=(double *)LCreceive(nord_in);
        if(id<nWorkers-1)
            s=(double *)LCreceive(sud_in);

        //start the computation of the matrix
        for(i=0; i<matrixH; i++)
        {
            for(j=0; j<matrixW; j++)
            {
                north=0;
                south=0;
                west=0;
                est=0;
                if(i==0) //first row
                {
                    if(id>0) //not the first worker
                        north=n[j];
                }
                else
                {
                    if(i==matrixH-1) //last row
                    {
                        if(id<nWorkers-1) //not the last worker
                            south=s[j];
                    }
                    else //a generic inner row
                    {
                        north=m[i-1][j];
                        south=m[i+1][j];
                    }
                }

                if(j>0 && j<(matrixW-1)) //inner column
                {
                    est=m[i][j-1];
                    west=m[i][j+1];
                }
                //obtain the elements involved in the computation
                //printf("[%d][%d] Calcolo con %f %f %f %f %f\n",i,j,m[i][j],north,south,west,est);
                m_next[i][j]=0.20*(m[i][j]+north+south+west+est);
                //convergence condition
                //printf("[%d][%d] a=%f, b= %f, %f\n",i,j,m_next[i][j],m[i][j],fabs(m_next[i][j]-m[i][j]));
                conv=conv || (fabs(m_next[i][j]-m[i][j])<EPS);
            }
        }

        /*
            Reduce: send the conv boolean to each other and receive a boolean
            from each other
        */
//        printf("[worker %d] invio reduce=%d\n",conv);
        for(i=0; i<nWorkers; i++)
        {
            if(i!=id)
                LCsend(red_out[i],(char *)&conv);
        }

        stop=conv;
        for(i=0; i<nWorkers; i++)
        {
            if(i!=id)
            {
                bool tmp=*((bool *) LCreceive(red_in[i]));
                stop=stop || tmp;
//                printf("[worker %d] ricevuta reduce da %d=%d\n",id,i,tmp);
            }
        }


        //exchange m and m_next for the next step
        double **tmp=m_next;
        m_next=m;
        m=tmp; //at the next step, m will the matrix compputed at this one



//        printf("[Worker %d] iterazione %d completata, conv=%d, reduce=%d\n",id,it,conv,stop);
        it++;
    }
    while(!stop && it<MAX_IT);
    //printf("[Worker %d] terminato, it=%d\n",id,it);
    //debug print
   /* for(i=0; i<matrixH; i++)
    {
        printf("[");
        for(j=0; j<matrixW; j++)
            printf(" %3.3f",m[i][j]);
        printf(" ]\n");
    }*/

    //destroys channel
    if(nord_in)
    {
        LCdestroy(nord_in);
        LCdetatch(nord_in);
    }
    if(nord_out)
    {
        LCdestroy(nord_out);
        LCdetatch(nord_out);
    }
    if(sud_in)
    {
        LCdestroy(sud_in);
        LCdetatch(sud_in);
    }
    if(sud_out)
    {
        LCdestroy(sud_out);
        LCdetatch(sud_out);
    }
    for(i=0; i<nWorkers; i++)
    {
        if(i!=id)
        {
            LCdestroy(red_in[i]);
            LCdestroy(red_out[i]);
            LCdetatch(red_in[i]);
            LCdetatch(red_out[i]);
        }
    }
    //s[j] = 0.20 * (el[j] + el[j-1] + el[j+1] + up[j] + dn[j]);
    //t = t || (fabs(s[j] - el[j]) > eps);
    if(id<nWorkers-1) //if i'm not the last worker exit
        exit(0);

}

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        fprintf(stderr,"usage %s matrixSize nWorker",argv[0]);
        return -1;
    }

    matrixSize = atoi(argv[1]);
    nWorkers=atoi(argv[2]);
    int id_ftok=0;

    if(matrixSize%nWorkers!=0)
    {
        fprintf(stderr, "Error: the matrix size must be divisible for the number of workers\n");
        exit(-1);
    }

    matrixH=matrixSize/nWorkers;
    matrixW=matrixSize;
    /*
        Create the channels for data exchange between workers. The data type
        is a vector of double, with dimension equal to the matrix size.
        Each worker (with the exception of the first and the last one) has
        4 channel: nord_in, nord_out, sud_in,sud_out
    */
    Channel tmpchan;
    key_t *keys = (key_t *)(malloc(sizeof(key_t) * (nWorkers-1) * 2 ));
    int i,j;
    for (i = 0; i < 2*(nWorkers-1); i++)
    {
        keys[i] = ftok(argv[0],id_ftok++);
        //printf("keys[%d] = %u\n",i,keys[i]);

        if(keys[i]==-1)
        {
            printf("Error creating a new key with parameters %s %d\n",argv[0],i);
            exit(-1);
        }

        tmpchan =  LCcreate_as( keys[i], (matrixSize * sizeof(double)) , 1, 1 );
        LCdetatch(tmpchan);

    }

    /*
        Create the channel for the reduce: in practice we have to build
        a crossbar between the worker. Thus, we create a directed graph, in which
        each edge is a channel
    */
    key_t **red_k=(key_t **)malloc(nWorkers*sizeof(key_t *));
    for(i=0; i<nWorkers; i++)
    {
        red_k[i]=(key_t *)malloc(nWorkers*sizeof(key_t));
    }
    int t,z;
    id_ftok=0;
    for(i=0; i<nWorkers; i++)
    {
        for(j=0; j<nWorkers; j++)
        {
            if(i!=j)
            {

               /* Create key with different parameter to avoid collision*/
                red_k[i][j]=ftok("/tmp",id_ftok++);
                if(red_k[i][j]==-1)
                {
                    printf("Error creating a new key with parameters %s %d\n",argv[0],i);
                    exit(-1);
                }
                //printf("Ho creato chiave reduce [%d][%d]=%d\n",i,j,red_k[i][j]);
                tmpchan =  LCcreate_as( red_k[i][j], (sizeof(bool)) , 1, 1 );
                LCdetatch(tmpchan);
            }
        }
    }


    /*
       Creation of the processes
    */


    int *pids=(int *)malloc(sizeof(int)*(nWorkers-1));
    for (i = 0; i < nWorkers-1; i++)
    {
        pid_t pid = fork();
        if(pid==0)  // Child
        {
            if(i==0) //first worker, it has no channel toward the top of it
                worker(i,0,0,keys[0],keys[1],red_k);
            else //generic worker (not the last one)
                worker(i,keys[2*(i-1)+1],keys[2*(i-1)],keys[2*i], keys[2*i+1],red_k);
        }
        else if (pid < 0)   // failed to fork
        {
            printf("Failed to fork\n");
            exit(1);
            // Throw exception
        }
        else
        {
            pids[i] = pid;
        }
    }


    struct timeval tmp_t;
    long start_tmill=0, end_tmill=0;
    gettimeofday(&tmp_t,NULL);
    start_tmill=tmp_t.tv_sec*1000+(tmp_t.tv_usec/1000);
    //act as last worker (no interface toward the bottom)
    if(nWorkers==1)
        worker(0,0,0,0,0,red_k); //1 worker, no neighbor
    else
        worker(nWorkers-1,keys[2*(nWorkers-2)+1],keys[2*(nWorkers-2)],0,0,red_k);


    // Wait for the workers to quit
    int childExitStatus;
    for (i = 0; i < nWorkers-1; i++)
    {
        printf("Waitpid %d \n",i);
        waitpid( pids[i] , &childExitStatus, 0);
        printf("Waitpid %d conclusa\n",i);
    }
    gettimeofday(&tmp_t,NULL);
    end_tmill=tmp_t.tv_sec*1000+(tmp_t.tv_usec/1000)-start_tmill;
    printf("Time elapsed (msec): %ld\n",end_tmill);

    FILE *out=fopen("tmp_res.dat","a");
    fprintf(out,"%d\n",end_tmill);
    fclose(out);
    return 0;
}
